package codegen.ui.nbvisual.visual;

import codegen.graphnetwork.convertor.TLink;
import org.netbeans.api.visual.action.ActionFactory;
import org.netbeans.api.visual.action.WidgetAction;
import org.netbeans.api.visual.anchor.Anchor;
import org.netbeans.api.visual.graph.GraphPinScene;
import org.netbeans.api.visual.graph.layout.GridGraphLayout;
import org.netbeans.api.visual.layout.LayoutFactory;
import org.netbeans.api.visual.layout.SceneLayout;
import org.netbeans.api.visual.router.Router;
import org.netbeans.api.visual.router.RouterFactory;
import org.netbeans.api.visual.widget.ConnectionWidget;
import org.netbeans.api.visual.widget.EventProcessingType;
import org.netbeans.api.visual.widget.LayerWidget;
import org.netbeans.api.visual.widget.Widget;
import java.awt.Color;
import java.awt.Image;
import java.awt.Point;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.swing.ImageIcon;
import org.apache.commons.collections15.BidiMap;
import org.apache.commons.collections15.bidimap.DualHashBidiMap;
import org.jgrapht.DirectedGraph;
import org.jgrapht.alg.CycleDetector;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.traverse.DepthFirstIterator;
import org.netbeans.api.visual.action.ConnectProvider;
import org.netbeans.api.visual.action.ConnectorState;
import org.netbeans.api.visual.action.ReconnectProvider;
import org.netbeans.api.visual.action.TextFieldInplaceEditor;
import org.netbeans.api.visual.anchor.AnchorShape;
import org.netbeans.api.visual.anchor.PointShape;
import org.netbeans.api.visual.widget.LabelWidget;
import org.netbeans.api.visual.widget.Scene;
import codegen.graphnetwork.model.*;
/**
 * This class represents a GraphPinScene for the VMD visualization style. Nodes, edges and pins are represented using
 * String class. The visualization is done by: CodeGenNodeWidget for nodes, CodeGenPinWidget for pins, ConnectionWidget
 * fro edges. <p> The scene has 4 layers: background, main, connection, upper. <p> The scene has following actions:
 * zoom, panning, rectangular selection.
 *
 * @author David Kaspar
 */
public class CodeGenGraphScene extends GraphPinScene<Node, Link, Value> {

    public static final String PIN_ID_DEFAULT_SUFFIX = "#default"; // NOI18N
    private GridLayerWidget gridLayer = new GridLayerWidget(this);
    private LayerWidget backgroundLayer = new LayerWidget(this);
    private LayerWidget mainLayer = new LayerWidget(this);
    private LayerWidget connectionLayer = new LayerWidget(this);
    private LayerWidget upperLayer = new LayerWidget(this);
    private LayerWidget helpLayer = new LayerWidget(this);
    private LayerWidget groupLayer = new LayerWidget(this);
    private LayerWidget interractionLayer = new LayerWidget(this);
    private LabelWidget toolTipBox = new LabelWidget(this);
    private Router router;
    private WidgetAction moveControlPointAction = ActionFactory.createOrthogonalMoveControlPointAction();
    private WidgetAction moveAction = ActionFactory.createMoveAction();
    private SceneLayout sceneLayout;
    private WidgetAction connectAction = ActionFactory.createConnectAction(interractionLayer, new SceneConnectProvider());
    private WidgetAction reconnectAction = ActionFactory.createReconnectAction(new SceneReconnectProvider());
    private long nodeCounter = 0;
    private long edgeCounter = 0;
    private HashMap<Anchor, Value> anchorPinMap = new HashMap<Anchor, Value>();
    private Node startNode;
    private Node endNode;
    /**
     * Keep a private JGraphT 's DirectedGraph to check the circle loop situation
     */
    private DirectedGraph<Value, TLink> directedGraph;
    private BidiMap<Link, TLink> tlinkMap = new DualHashBidiMap<Link, TLink>();
    private Router directRouter;
    CycleDetector cycleDetector;

    private void setToolTipBoxText(String string) {
        toolTipBox.setLabel(string);

    }

    private class SceneConnectProvider implements ConnectProvider {

        private Value source = null;
        private Value target = null;

        public boolean isSourceWidget(Widget sourceWidget) {
            Object object = null;

            if (sourceWidget instanceof CircleWidget) {
                object = findObject(VisualHelper.upToFind(sourceWidget, CodeGenPinWidget.class));
                //System.out.println(" Click a Select Widget!");
            } else {
                object = findObject(sourceWidget);
            }
            source = null;

            source = isPin(object) ? (Value) object : null;
            // Source have to be Value object!

            if (source != null) {
                return true;
            } else {
                return false;
            }
        }

        public ConnectorState isTargetWidget(Widget sourceWidget, Widget targetWidget) {
            Object object = null;

            if (targetWidget instanceof CircleWidget) {
                Widget pinWidget = VisualHelper.upToFind(targetWidget, CodeGenPinWidget.class);
                object = findObject(pinWidget);
            } else {
                object = findObject(targetWidget);
            }

            target = isPin(object) ? (Value) object : null;

            if (target != null) {
                //Object sourceNode = findObject(VisualHelper.upToFind(sourceWidget, CodeGenNodeWidget.class));
                //Object targetNode = findObject(VisualHelper.upToFind(targetWidget, CodeGenNodeWidget.class));
                if (source.isInput) {
                    return validConection(target, source);
                } else {
                    return validConection(source, target);
                }

            }
            return ConnectorState.REJECT_AND_STOP;
        }

        public boolean hasCustomTargetWidgetResolver(Scene scene) {
            return false;
        }

        public Widget resolveTargetWidget(Scene scene, Point sceneLocation) {
            return null;
        }

        public void createConnection(Widget sourceWidget, Widget targetWidget) {
            String edge = "edge" + edgeCounter++;
            Link newLink = new Link(edge);
            setConnectionValues(newLink, source, target);
            addEdge(newLink);

            if (source.isInput) {
                setEdgeSource(newLink, target);
                setEdgeTarget(newLink, source);
            } else {
                setEdgeSource(newLink, source);
                setEdgeTarget(newLink, target);
            }
            //debugJGraphT();
            //newLink.
        }
    }

    private class SceneReconnectProvider implements ReconnectProvider {

        Link edge;
        Value originalPin;
        Value replacementPin;

        public void reconnectingStarted(ConnectionWidget connectionWidget, boolean reconnectingSource) {
        }

        public void reconnectingFinished(ConnectionWidget connectionWidget, boolean reconnectingSource) {
        }

        public boolean isSourceReconnectable(ConnectionWidget connectionWidget) {
            Object object = findObject(connectionWidget);
            edge = isEdge(object) ? (Link) object : null;
            originalPin = edge != null ? getEdgeSource(edge) : null;
            return originalPin != null;
        }

        public boolean isTargetReconnectable(ConnectionWidget connectionWidget) {
            Object object = findObject(connectionWidget);
            edge = isEdge(object) ? (Link) object : null;
            originalPin = edge != null ? getEdgeTarget(edge) : null;
            return originalPin != null;
        }

        public ConnectorState isReplacementWidget(ConnectionWidget connectionWidget, Widget replacementWidget, boolean reconnectingSource) {
            Object object = findObject(replacementWidget);
            replacementPin = isPin(object) ? (Value) object : null;
            if (replacementPin != null) {
                return validConection(originalPin, replacementPin);
            }
            return object != null ? ConnectorState.REJECT_AND_STOP : ConnectorState.REJECT;
        }

        public boolean hasCustomReplacementWidgetResolver(Scene scene) {
            return false;
        }

        public Widget resolveReplacementWidget(Scene scene, Point sceneLocation) {
            return null;
        }

        public void reconnect(ConnectionWidget connectionWidget, Widget replacementWidget, boolean reconnectingSource) {
            if (replacementWidget == null) {
                removeEdge(edge);
            } else if (reconnectingSource) {
                setEdgeSource(edge, replacementPin);
            } else {
                setEdgeTarget(edge, replacementPin);
            }
        }
    }

    /**
     * Creates a VMD graph scene with a specific color .
     *
     * @param the color
     */
    public CodeGenGraphScene() {
        setKeyEventProcessingType(EventProcessingType.FOCUSED_WIDGET_AND_ITS_PARENTS);

        //addChild(gridLayer);
        addChild(backgroundLayer);
        addChild(mainLayer);
        addChild(connectionLayer);
        addChild(upperLayer);

        addChild(interractionLayer);
        addChild(helpLayer);
        helpLayer.setLayout(LayoutFactory.createAbsoluteLayout());
        helpLayer.addChild(toolTipBox);
        setToolTipBoxText("Click to add a Node. Drag to Move.");
        toolTipBox.setForeground(Color.yellow);
        router = RouterFactory.createOrthogonalSearchRouter(mainLayer, connectionLayer);
        //backgroundLayer.setOpaque(true);
        //backgroundLayer.setBackground(Color.darkGray);
        getActions().addAction(ActionFactory.createZoomAction());
        getActions().addAction(ActionFactory.createPanAction());
        getActions().addAction(ActionFactory.createRectangularSelectAction(this,
                backgroundLayer));
        setBackground(Color.darkGray);
        getActions().addAction(ActionFactory.createCycleObjectSceneFocusAction());
        sceneLayout = LayoutFactory.createSceneGraphLayout(this, new GridGraphLayout<Node, Link>().setChecker(true));
        /*
         Image GRID_BG = loadImageFromResource("Textures/grid/checkboard.png");
         ImageWidget imageWidget = new ImageWidget(this, GRID_BG);
         imageWidget.setPaintAsDisabled(false);
         //imageWidget.setImage(GRID_BG);
         imageWidget.setMinimumSize(new Dimension(100, 100));
         imageWidget.setOpaque(true);
         imageWidget.setPreferredLocation(new Point(200, 300));
        
         addChild(imageWidget);
         */

        // Setup JGraphT directedGraph
        directedGraph = new DefaultDirectedGraph<Value, TLink>(TLink.class);
        cycleDetector = new CycleDetector(directedGraph);
        //ResourceBundle lang = ResourceBundle.getBundle("codegen.nbvisual.visual", Locale.ENGLISH, CodeGenGraphScene.class.getClassLoader());

        //setToolTipBoxText(lang.getString("RClick_Add_Node"));
    }

    private class LabelTextFieldEditor implements TextFieldInplaceEditor {

        public boolean isEnabled(Widget widget) {
            return true;
        }

        public String getText(Widget widget) {
            return ((LabelWidget) widget).getLabel();
        }

        public void setText(Widget widget, String text) {
            ((LabelWidget) widget).setLabel(text);
        }
    }

    private Image loadImageFromResource(String path) {
        ClassLoader cldr = this.getClass().getClassLoader();
        URL imageURL = cldr.getResource(path);

        ImageIcon icon = new ImageIcon(imageURL);
        if (icon.getImage() == null) {
            System.out.println(" Null Image !");
        }
        return icon.getImage();
    }

    /**
     * Implements attaching a widget to a node. The widget is CodeGenNodeWidget and has object-hover, select, popup-menu
     * and move actions.
     *
     * @param node the node
     * @return the widget attached to the node
     */
    protected Widget attachNodeWidget(Node node) {
        CodeGenNodeWidget widget = new CodeGenNodeWidget(this);
        mainLayer.addChild(widget);

        //widget.getHeader().getActions().addAction(createObjectHoverAction());
        widget.setNodeName(node.getName());
        widget.getActions().addAction(createSelectAction());
        widget.getActions().addAction(moveAction);
        widget.getActions().addAction(createObjectHoverAction());
//        label.getActions ().addAction (createSelectAction ());

        return widget;
    }

    /**
     * Implements attaching a widget to a pin. The widget is CodeGenPinWidget and has object-hover and select action.
     * The the node id ends with "#default" then the pin is the default pin of a node and therefore it is non-visual.
     *
     * @param node the node
     * @param pin the pin
     * @return the widget attached to the pin, null, if it is a default pin
     */
    protected Widget attachPinWidget(Node node, Value pin) {
        if (pin.defaultPin) {
            return null;
        }

        CodeGenPinWidget widget = new CodeGenPinWidget(this, pin);
        ((CodeGenNodeWidget) findWidget(node)).attachPinWidget(widget);
        widget.setPinName(pin.getName());
        widget.getActions().addAction(createObjectHoverAction());
        widget.getActions().addAction(createSelectAction());
        widget.getSelectPinWidget().getActions().addAction(connectAction);
        //widget.getActions().addAction(connectAction);
        return widget;
    }

    /**
     * Implements attaching a widget to an edge. the widget is ConnectionWidget and has object-hover, select and
     * move-control-point actions.
     *
     * @param edge the edge
     * @return the widget attached to the edge
     */
    protected Widget attachEdgeWidget(Link edge) {
        CurveConnectionWidget connectionWidget = new CurveConnectionWidget(this);
        //FreeConnectionWidget connectionWidget = new FreeConnectionWidget(this);
        connectionWidget.setRouter(router);
        connectionLayer.addChild(connectionWidget);

        connectionWidget.setTargetAnchorShape(AnchorShape.TRIANGLE_FILLED);
        connectionWidget.setEndPointShape(PointShape.SQUARE_FILLED_BIG);

        connectionWidget.getActions().addAction(reconnectAction);
        connectionWidget.getActions().addAction(createObjectHoverAction());
        connectionWidget.getActions().addAction(createSelectAction());
        connectionWidget.getActions().addAction(moveControlPointAction);



        return connectionWidget;
    }

    /**
     * Attaches an anchor of a source pin an edge. The anchor is a ProxyAnchor that switches between the anchor attached
     * to the pin widget directly and the anchor attached to the pin node widget based on the minimize-state of the
     * node.
     *
     * @param edge the edge
     * @param oldSourcePin the old source pin
     * @param sourcePin the new source pin
     */
    protected void attachEdgeSourceAnchor(Link edge, Value oldSourcePin, Value sourcePin) {
        ((ConnectionWidget) findWidget(edge)).setSourceAnchor(getPinAnchor(sourcePin));
    }

    /**
     * Attaches an anchor of a target pin an edge. The anchor is a ProxyAnchor that switches between the anchor attached
     * to the pin widget directly and the anchor attached to the pin node widget based on the minimize-state of the
     * node.
     *
     * @param edge the edge
     * @param oldTargetPin the old target pin
     * @param targetPin the new target pin
     */
    protected void attachEdgeTargetAnchor(Link edge, Value oldTargetPin, Value targetPin) {
        ((ConnectionWidget) findWidget(edge)).setTargetAnchor(getPinAnchor(targetPin));
    }

    private Anchor getPinAnchor(Value pin) {
        if (pin == null) {
            return null;
        }
        CodeGenNodeWidget nodeWidget = (CodeGenNodeWidget) findWidget(getPinNode(pin));
        CodeGenPinWidget pinMainWidget = (CodeGenPinWidget) findWidget(pin);
        Anchor anchor;
        if (pinMainWidget != null) {
            //anchor = AnchorFactory.createDirectionalAnchor(pinMainWidget, AnchorFactory.DirectionalAnchorKind.HORIZONTAL, 8);
            //anchor = AnchorFactory.createFixedAnchor(new Point(128,12));
            anchor = pinMainWidget.getPinAnchor();
            anchor = nodeWidget.createAnchorPin(anchor);
            anchorPinMap.put(anchor, pin);
        } else {
            anchor = nodeWidget.getNodeAnchor();
        }
        return anchor;

    }

    public Value findPinByAnchor(Anchor anchor) {
        return anchorPinMap.get(anchor);
    }

    /**
     * Invokes layout of the scene.
     */
    public void layoutScene() {
        sceneLayout.invokeLayout();
    }

    public Node getNodeById(String id) {
        for (Node aNode : getNodes()) {
            if (aNode.getName().equals(id)) {
                return aNode;
            }
        }
        return null;
    }

    public Value getPinById(String id) {
        for (Iterator<Value> it = getPins().iterator(); it.hasNext();) {
            Value aPin = it.next();
            if (aPin.getName().equals(id)) {
                return aPin;
            }
        }
        return null;
    }

    public Node getStartNode() {
        return startNode;
    }

    public Node getEndNode() {
        return endNode;
    }

    public void setStartNode(Node startNode) {
        this.startNode = startNode;
    }

    public void setEndNode(Node endNode) {
        this.endNode = endNode;
    }

    @Override
    protected void notifyPinAdded(Node node, Value pin, Widget widget) {
        super.notifyPinAdded(node, pin, widget);

        // For Code gen specific underground operation
        if (pin.isInput) {
            node.addInput(pin);
        } else {
            node.addOutput(pin);
        }
        pin.parent = node;
        // For JGraphT
        directedGraph.addVertex(pin);
        Collection<Value> values = this.getNodePins(node);
        for (Value v : values) {
            directedGraph.addVertex(v);
            // Add dependency between input and output of a Node
            if (v.isInput) {
                for (Value v2 : values) {
                    if (v != v2 && !v2.isInput) {
                        directedGraph.addEdge(v, v2);
                    }
                }
            }
        }

        //printOutJGraphT();
    }

    @Override
    protected void notifyEdgeAdded(Link edge, Widget widget) {
        super.notifyEdgeAdded(edge, widget);
        // For Code gen specific underground operation
        // For JGraphT
        Value source;
        Value target;
        //source= getEdgeSource(edge);
        //target= getEdgeTarget(edge);

        source = edge.getOutput();
        target = edge.getInput();
        TLink tlink = directedGraph.addEdge(source, target);
        tlinkMap.put(edge, tlink);

        //printOutJGraphT();
    }

    @Override
    protected void detachEdgeWidget(Link edge, Widget widget) {
        super.detachEdgeWidget(edge, widget);

        directedGraph.removeEdge(tlinkMap.get(edge));
        tlinkMap.remove(edge);
    }

    @Override
    protected void detachPinWidget(Value pin, Widget widget) {
        super.detachPinWidget(pin, widget);

        directedGraph.removeVertex(pin);
    }

    public void setConnectionValues(Link newLink, Value source, Value target) {
        //Value source = this.getEdgeSource(newLink);
        //Value target = this.getEdgeTarget(newLink);

        source.link = newLink;
        source.linked = true;
        target.link = newLink;
        target.linked = true;

        if (source.isInput) {
            newLink.setInput(source);
            newLink.setOutput(target);
        } else {
            newLink.setInput(target);
            newLink.setOutput(source);
        }

    }

    private boolean isCompatibleValueType(Value source, Value target) {
        return source.type.equals(target.type);
    }

    public ConnectorState validConection(Value source, Value target) {
        Node sourceNode = source.parent;
        Node targetNode = target.parent;
        // Check if they are in the same Node!
        if (sourceNode != null && targetNode != null) {
            if (sourceNode == targetNode) {
                return ConnectorState.REJECT;
            }
        } else {
            return ConnectorState.REJECT_AND_STOP;
        }

        // Target should be Value object
        // One should be Input and other should be output.
        if (!source.equals(target)) {
            if ((source.isInput && !target.isInput) || (!source.isInput && target.isInput)) {
                if (isCompatibleValueType(source, target)) {
                    if (!isALoop(source, target)) {
                        setToolTipBoxText("Click to add a Node. Drag to move ");
                        return ConnectorState.ACCEPT;
                    } else {
                        setToolTipBoxText("Loop detected ");
                        return ConnectorState.REJECT;
                    }

                } else {
                    setToolTipBoxText("Incompatible Type ");
                    return ConnectorState.REJECT;
                }
            } else {
                setToolTipBoxText("Input and Output pair ! ");
                return ConnectorState.REJECT;
            }
        } else {
            setToolTipBoxText("Source and Target should be in different Node ");
            return ConnectorState.REJECT;
        }
    }

    private boolean isALoop(Value source, Value target) {
        // tempory add an edge
        directedGraph.addEdge(source, target);
        boolean result = cycleDetector.detectCycles();
        System.out.println(" Is loop : " + result);
        directedGraph.removeEdge(source, target);
        return result;
    }

    void printOutJGraphT() {
        //Print out the graph to be sure it's really complete
        DepthFirstIterator<Value, TLink> iter =
                new DepthFirstIterator<Value, TLink>(directedGraph);

        while (iter.hasNext()) {
            Value vertex = iter.next();
            System.out.println("Vertex (" + vertex.getName() + ") has :");
            for (TLink tl : directedGraph.edgesOf(vertex)) {
                Link l = tlinkMap.getKey(tl);
                if (l != null) {
                    System.out.println(l.getInput().getName() + "->" + l.getOutput().getName());
                }
            }
        }
    }

    Anchor getPinSelectAnchor(Value pin) {
        CodeGenNodeWidget nodeWidget = (CodeGenNodeWidget) findWidget(getPinNode(pin));
        CodeGenPinWidget pinMainWidget = (CodeGenPinWidget) findWidget(pin);
        if (pinMainWidget != null) {
            return pinMainWidget.getPinAnchor();
        }
        return null;
    }

    public void debugJGraphT() {
        helpLayer.removeChildren();
        directRouter = RouterFactory.createDirectRouter();
        System.out.println(" Directed graph links : " + directedGraph.edgeSet().size());
        for (TLink tl : directedGraph.edgeSet()) {
            Value first = directedGraph.getEdgeSource(tl);
            Value last = directedGraph.getEdgeTarget(tl);

            Anchor anchorFirst = getPinSelectAnchor(first);
            Anchor anchorLast = getPinSelectAnchor(last);
            ConnectionWidget conWidget = new ConnectionWidget(this);
            conWidget.setRouter(directRouter);
            conWidget.setSourceAnchor(anchorFirst);
            conWidget.setTargetAnchor(anchorLast);
            conWidget.setTargetAnchorShape(AnchorShape.TRIANGLE_OUT);
            //conWidget.setEndPointShape(PointShape.NONE);
            helpLayer.addChild(conWidget);
        }

    }
}
